This document uses the Sigle/Robinson book on Tidy Text Mining. Chapter 7 has a very useful case study for comparing twitter archives.

load libraries

library(rtweet)
library(tidyverse)
library(tidytext)
library(wordcloud2)

Get Tweets

The other notebook, 01_gather-tweets.Rmd, covers tweet gathering in more detail

search_tweets

tweet_collection <- search_tweets("lego star wars", n=10000, lang = "en")

Downloading [>----------------------------------------]   2%
Downloading [>----------------------------------------]   3%
Downloading [=>---------------------------------------]   4%
Downloading [=>---------------------------------------]   5%
Downloading [=>---------------------------------------]   6%
Downloading [==>--------------------------------------]   7%
Downloading [==>--------------------------------------]   8%
Downloading [===>-------------------------------------]   9%
Downloading [===>-------------------------------------]  10%
Downloading [====>------------------------------------]  11%
Downloading [====>------------------------------------]  12%
Downloading [====>------------------------------------]  13%
Downloading [=====>-----------------------------------]  14%
Downloading [=====>-----------------------------------]  15%
Downloading [======>----------------------------------]  16%
Downloading [======>----------------------------------]  17%
Downloading [======>----------------------------------]  18%
Downloading [=======>---------------------------------]  19%
Downloading [=======>---------------------------------]  20%
Downloading [========>--------------------------------]  21%
Downloading [========>--------------------------------]  22%
Downloading [========>--------------------------------]  23%
Downloading [=========>-------------------------------]  24%
Downloading [=========>-------------------------------]  25%
Downloading [==========>------------------------------]  26%
Downloading [==========>------------------------------]  27%
Downloading [==========>------------------------------]  28%
Downloading [===========>-----------------------------]  29%
Downloading [===========>-----------------------------]  30%
Downloading [============>----------------------------]  31%
Downloading [============>----------------------------]  32%
Downloading [=============>---------------------------]  33%
Downloading [=============>---------------------------]  34%
Downloading [=============>---------------------------]  35%
Downloading [==============>--------------------------]  36%
Downloading [==============>--------------------------]  37%
Downloading [===============>-------------------------]  38%
Downloading [===============>-------------------------]  39%
Downloading [===============>-------------------------]  40%
Downloading [================>------------------------]  41%
Downloading [================>------------------------]  42%
Downloading [=================>-----------------------]  43%
Downloading [=================>-----------------------]  44%
Downloading [=================>-----------------------]  45%
Downloading [==================>----------------------]  46%
Downloading [==================>----------------------]  47%
Downloading [===================>---------------------]  48%
Downloading [===================>---------------------]  49%
Downloading [===================>---------------------]  50%
Downloading [====================>--------------------]  51%
Downloading [====================>--------------------]  52%
Downloading [=====================>-------------------]  53%
Downloading [=====================>-------------------]  54%
Downloading [======================>------------------]  55%
Downloading [======================>------------------]  56%
Downloading [======================>------------------]  57%
Downloading [=======================>-----------------]  58%
Downloading [=======================>-----------------]  59%
Downloading [========================>----------------]  60%
Downloading [========================>----------------]  61%
Downloading [========================>----------------]  62%
Downloading [=========================>---------------]  63%
Downloading [=========================>---------------]  64%
Downloading [==========================>--------------]  65%
Downloading [==========================>--------------]  66%
Downloading [==========================>--------------]  67%
Downloading [===========================>-------------]  68%
Downloading [===========================>-------------]  69%
Downloading [============================>------------]  70%
Downloading [============================>------------]  71%
Downloading [=============================>-----------]  72%
Downloading [=============================>-----------]  73%
Downloading [=============================>-----------]  74%
Downloading [==============================>----------]  75%
Downloading [==============================>----------]  76%
Downloading [===============================>---------]  77%
Downloading [===============================>---------]  78%
Downloading [===============================>---------]  79%
Downloading [================================>--------]  80%
Downloading [================================>--------]  81%
Downloading [=================================>-------]  82%
Downloading [=================================>-------]  83%
Downloading [=================================>-------]  84%
Downloading [==================================>------]  85%
Downloading [==================================>------]  86%
Downloading [===================================>-----]  87%
Downloading [===================================>-----]  88%
Downloading [===================================>-----]  89%
Downloading [====================================>----]  90%
Downloading [====================================>----]  91%
Downloading [=====================================>---]  92%
Downloading [=====================================>---]  93%
tweet_collection.orig <- tweet_collection
tweet_collection <- tweet_collection %>% 
  filter(is_retweet == "FALSE") 

tweet_collection

Tokenize tweets

tweets_by_tweeter <- tweet_collection %>% 
  group_by(screen_name) %>% 
  mutate(line = row_number()) %>% 
  ungroup()

tweets_by_tweeter %>% 
  count(screen_name, sort = TRUE)

# glimpse(tweets_by_tweeter)

"Because we have kept text such as hashtags and usernames in the dataset, we can’t use a simple anti_join() to remove stop words. Instead, we can take the approach shown in the filter() line that uses str_detect() from the stringr package. – https://www.tidytextmining.com/twitter.html

tweets_tokenized <- tweets_by_tweeter %>% 
  select(text, screen_name, line) %>% 
  unnest_tokens(word, text, token = "tweets") %>%
  filter(!word %in% stop_words$word,
         !word %in% str_remove_all(stop_words$word, "'"),
         str_detect(word, "[a-z]")) 
Using `to_lower = TRUE` with `token = 'tweets'` may not preserve URLs.
tweets_tokenized

stop words

The rtweet package has a special stopwords dataset that may come in handy with tweets….

head(stopwordslangs)
tweets_tokenized %>% 
  count(word, sort = TRUE, name = "freq") %>% 
  filter(!str_detect(word, "^\\@")) %>% 
  anti_join(stopwordslangs)           # anti_join(tidytext::get_stopwords())
Joining, by = "word"

Word frequencies

Calculate word frequency

frequency <- tweets_tokenized %>% 
  group_by(screen_name) %>% 
  count(word, sort = TRUE) %>% 
  left_join(tweets_tokenized %>% 
              group_by(screen_name) %>% 
              summarise(total = n())) %>%
  mutate(freq = n/total)
`summarise()` ungrouping output (override with `.groups` argument)
Joining, by = "screen_name"
frequency

"This is a nice and tidy data frame but we would actually like to plot those frequencies on the x- and y-axes of a plot, so we will need to use spread() from tidyr make a differently shaped data frame. – https://www.tidytextmining.com/twitter.html

pivot_wider

frequency <- frequency %>% 
  select(screen_name, word, freq) %>% 
  pivot_wider(names_from = screen_name, values_from = freq) #, values_fill = 0)

frequency 

Visualize

Word cloud

tweets_tokenized %>% 
  # group_by(screen_name) %>% 
  count(word, sort = TRUE, name = "freq") %>% 
  head(500) %>%
  filter(!str_detect(word, "^\\@")) %>% 
  anti_join(stopwordslangs)  %>% 
  wordcloud2(size = 2)
Joining, by = "word"

Frequencies are often better expressed with bars

tweets_tokenized %>% 
  count(word, sort = TRUE, name = "freq") %>% 
  filter(!str_detect(word, "^\\@")) %>% 
  slice_head(n = 30) %>% 
  ggplot(aes(freq, fct_reorder(word, freq))) +
  geom_col()


tweets_tokenized %>% 
  count(word, sort = TRUE, name = "freq") %>% 
  anti_join(stopwordslangs) %>%                    # twitter stop words.
  filter(!str_detect(word, "^\\@")) %>% 
  slice_head(n = 30) %>% 
  ggplot(aes(freq, fct_reorder(word, freq))) +
  geom_col()
Joining, by = "word"

Or plot the word frequency ratios of two tweeters https://www.tidytextmining.com/twitter.html#word-frequencies-1

ggplot(frequency, aes(RGONZALEZ1978, BrickFanatics)) +
  geom_jitter(alpha = 0.1, size = 2.5, width = 0.25, height = 0.25) +
  geom_text(aes(label = word), check_overlap = TRUE, vjust = 1.5) +
  scale_x_log10(labels = scales::percent_format()) +
  scale_y_log10(labels = scales::percent_format()) +
  geom_abline(color = "firebrick")


# fs::dir_create("images")
# ggsave("images/dukeball.png")

# "CBBCent1" | screen_name == "Adam_Bradford1
# marchmadness  TheAndyKatz

# RGONZALEZ1978
# <dbl>
# BrickFanatics
# <dbl>


ggplot(frequency, aes(BrickFanatics, fantasysite)) +
  geom_jitter(alpha = 0.1, size = 2.5, width = 0.25, height = 0.25) +
  geom_text(aes(label = word), check_overlap = TRUE, vjust = 1.5) +
  scale_x_log10(labels = scales::percent_format()) +
  scale_y_log10(labels = scales::percent_format()) +
  geom_abline(color = "firebrick")

Word Usage

https://www.tidytextmining.com/twitter.html#comparing-word-usage

tweets_by_tweeter %>% 
  summarise(min_date = min(created_at), max_date = max(created_at))
word_ratios <- tweets_tokenized %>%
  # filter(screen_name == "CBBCent1" | screen_name == "Adam_Bradford14") %>%
  filter(screen_name == "BrickFanatics" | screen_name == "fantasysite") %>% 
  filter(!str_detect(word, "^@")) %>%
  count(word, screen_name) %>%
  group_by(word) %>%
  filter(sum(n) >= 2) %>%
  ungroup() %>%
  pivot_wider(names_from = screen_name, values_from = n, values_fill = 0) %>%
  mutate_if(is.numeric, list(~(. + 1) / (sum(.) + 1))) %>%
  mutate(logratio = log(BrickFanatics / fantasysite)) %>%
  arrange(desc(logratio))

word_ratios

equal usage

word_ratios %>% 
  arrange(abs(logratio))
word_ratios %>%
  group_by(logratio < 0) %>%  # < 0) %>%
  top_n(15, abs(logratio)) %>%
  ungroup() %>%
  mutate(word = reorder(word, logratio)) %>%
  ggplot(aes(word, logratio, fill = logratio < 0)) +
  geom_col() + #show.legend = FALSE) +
  coord_flip() +
  ylab("log odds ratio (BrickFanatics/fantasysite)") +
  # scale_fill_discrete(name = "", labels = c("BrickFanatics", "fantasysite")) +
  scale_fill_brewer(palette = "Dark2", name = "", labels = c("BrickFanatics", "fantasysite"))

Favorites and retweets

https://www.tidytextmining.com/twitter.html#favorites-and-retweets

Changes in word use

https://www.tidytextmining.com/twitter.html#changes-in-word-use

Term Document Matrix

https://www.tidytextmining.com/tfidf.html#the-bind_tf_idf-function

tweet_words <- tweets_by_tweeter %>% 
  select(screen_name, text, status_id, user_id) %>%  
  unnest_tokens(word, text, token = "tweets") %>% 
  filter(!str_detect(word, "^\\@")) %>%
  filter(!str_detect(word, "^http")) %>%
  anti_join(stopwordslangs)  %>% 
  count(word, tweeter = screen_name, sort = TRUE)
Using `to_lower = TRUE` with `token = 'tweets'` may not preserve URLs.
Joining, by = "word"
tweet_words

total_words <- tweet_words %>%
  group_by(tweeter) %>%
  summarize(total = sum(n)) %>% 
  arrange(-total)
`summarise()` ungrouping output (override with `.groups` argument)
total_words

tweet_words <- left_join(tweet_words, total_words)
Joining, by = "tweeter"
tweet_words
tweet_words %>% 
  bind_tf_idf(word, tweeter, n)
tweet_words %>% 
  bind_tf_idf(word, tweeter, n) %>% 
  arrange(desc(tf_idf)) %>% 
  mutate(word = factor(word, levels = rev(unique(word)))) %>% 
  filter(n > 10) %>% 
  # group_by(tweeter) %>% 
  # top_n(2) %>% 
  # ungroup() %>% 
  ggplot(aes(word, tf_idf)) +
  geom_col() +
  facet_wrap(~ tweeter) +
  coord_flip()

Resource list

LS0tDQp0aXRsZTogIlJ0d2VldCINCnN1YnRpdGxlOiAiYW4gUmZ1biBkZW1vbnN0cmF0aW9uIg0KYXV0aG9yOiAiSm9obiBMaXR0bGUiDQpkYXRlOiAnYHIgU3lzLkRhdGUoKWAnDQpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sNCi0tLQ0KDQpUaGlzIGRvY3VtZW50IHVzZXMgdGhlIFNpZ2xlL1JvYmluc29uIGJvb2sgb24gW19UaWR5IFRleHQgTWluaW5nX10oaHR0cHM6Ly93d3cudGlkeXRleHRtaW5pbmcuY29tLykuIENoYXB0ZXIgNyBoYXMgYSB2ZXJ5IHVzZWZ1bCBjYXNlIHN0dWR5IGZvciBjb21wYXJpbmcgdHdpdHRlciBhcmNoaXZlcy4NCiANCg0KIyMgbG9hZCBsaWJyYXJpZXMNCg0KYGBge3IgbGlicmFyaWVzLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeShydHdlZXQpDQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmxpYnJhcnkodGlkeXRleHQpDQpsaWJyYXJ5KHdvcmRjbG91ZDIpDQpgYGANCg0KIyMgR2V0IFR3ZWV0cyANCg0KVGhlIG90aGVyIG5vdGVib29rLCAwMV9nYXRoZXItdHdlZXRzLlJtZCwgY292ZXJzIHR3ZWV0IGdhdGhlcmluZyBpbiBtb3JlIGRldGFpbA0KDQpzZWFyY2hfdHdlZXRzDQpgYGB7ciBnZXRUd2VldHN9DQp0d2VldF9jb2xsZWN0aW9uIDwtIHNlYXJjaF90d2VldHMoImxlZ28gc3RhciB3YXJzIiwgbj0xMDAwMCwgbGFuZyA9ICJlbiIpDQoNCnR3ZWV0X2NvbGxlY3Rpb24ub3JpZyA8LSB0d2VldF9jb2xsZWN0aW9uDQpgYGANCg0KDQpgYGB7ciBwcm9jZXNzIFR3ZWV0c30NCnR3ZWV0X2NvbGxlY3Rpb24gPC0gdHdlZXRfY29sbGVjdGlvbiAlPiUgDQogIGZpbHRlcihpc19yZXR3ZWV0ID09ICJGQUxTRSIpIA0KDQp0d2VldF9jb2xsZWN0aW9uDQpgYGANCg0KDQojIyBUb2tlbml6ZSB0d2VldHMNCg0KYGBge3IgY29ycHVzMnZlY3Rvcn0NCnR3ZWV0c19ieV90d2VldGVyIDwtIHR3ZWV0X2NvbGxlY3Rpb24gJT4lIA0KICBncm91cF9ieShzY3JlZW5fbmFtZSkgJT4lIA0KICBtdXRhdGUobGluZSA9IHJvd19udW1iZXIoKSkgJT4lIA0KICB1bmdyb3VwKCkNCg0KdHdlZXRzX2J5X3R3ZWV0ZXIgJT4lIA0KICBjb3VudChzY3JlZW5fbmFtZSwgc29ydCA9IFRSVUUpDQoNCiMgZ2xpbXBzZSh0d2VldHNfYnlfdHdlZXRlcikNCmBgYA0KDQoNCj4gIkJlY2F1c2Ugd2UgaGF2ZSBrZXB0IHRleHQgc3VjaCBhcyBoYXNodGFncyBhbmQgdXNlcm5hbWVzIGluIHRoZSBkYXRhc2V0LCB3ZSBjYW7igJl0IHVzZSBhIHNpbXBsZSBhbnRpX2pvaW4oKSB0byByZW1vdmUgc3RvcCB3b3Jkcy4gSW5zdGVhZCwgd2UgY2FuIHRha2UgdGhlIGFwcHJvYWNoIHNob3duIGluIHRoZSBmaWx0ZXIoKSBsaW5lIHRoYXQgdXNlcyBzdHJfZGV0ZWN0KCkgZnJvbSB0aGUgc3RyaW5nciBwYWNrYWdlLiAtLSBodHRwczovL3d3dy50aWR5dGV4dG1pbmluZy5jb20vdHdpdHRlci5odG1sDQoNCg0KYGBge3IgdG9rZW5pemVkIHR3ZWV0c30NCnR3ZWV0c190b2tlbml6ZWQgPC0gdHdlZXRzX2J5X3R3ZWV0ZXIgJT4lIA0KICBzZWxlY3QodGV4dCwgc2NyZWVuX25hbWUsIGxpbmUpICU+JSANCiAgdW5uZXN0X3Rva2Vucyh3b3JkLCB0ZXh0LCB0b2tlbiA9ICJ0d2VldHMiKSAlPiUNCiAgZmlsdGVyKCF3b3JkICVpbiUgc3RvcF93b3JkcyR3b3JkLA0KICAgICAgICAgIXdvcmQgJWluJSBzdHJfcmVtb3ZlX2FsbChzdG9wX3dvcmRzJHdvcmQsICInIiksDQogICAgICAgICBzdHJfZGV0ZWN0KHdvcmQsICJbYS16XSIpKSANCg0KdHdlZXRzX3Rva2VuaXplZA0KYGBgDQoNCiMjIHN0b3Agd29yZHMNCg0KVGhlIGBydHdlZXRgIHBhY2thZ2UgaGFzIGEgc3BlY2lhbCBzdG9wd29yZHMgZGF0YXNldCB0aGF0IG1heSBjb21lIGluIGhhbmR5IHdpdGggdHdlZXRzLi4uLg0KDQpgYGB7cn0NCmhlYWQoc3RvcHdvcmRzbGFuZ3MpDQpgYGANCg0KDQpgYGB7cn0NCnR3ZWV0c190b2tlbml6ZWQgJT4lIA0KICBjb3VudCh3b3JkLCBzb3J0ID0gVFJVRSwgbmFtZSA9ICJmcmVxIikgJT4lIA0KICBmaWx0ZXIoIXN0cl9kZXRlY3Qod29yZCwgIl5cXEAiKSkgJT4lIA0KICBhbnRpX2pvaW4oc3RvcHdvcmRzbGFuZ3MpICAgICAgICAgICAjIGFudGlfam9pbih0aWR5dGV4dDo6Z2V0X3N0b3B3b3JkcygpKQ0KYGBgDQoNCg0KIyMgV29yZCBmcmVxdWVuY2llcw0KDQojIyMgQ2FsY3VsYXRlIHdvcmQgZnJlcXVlbmN5DQoNCmBgYHtyfQ0KZnJlcXVlbmN5IDwtIHR3ZWV0c190b2tlbml6ZWQgJT4lIA0KICBncm91cF9ieShzY3JlZW5fbmFtZSkgJT4lIA0KICBjb3VudCh3b3JkLCBzb3J0ID0gVFJVRSkgJT4lIA0KICBsZWZ0X2pvaW4odHdlZXRzX3Rva2VuaXplZCAlPiUgDQogICAgICAgICAgICAgIGdyb3VwX2J5KHNjcmVlbl9uYW1lKSAlPiUgDQogICAgICAgICAgICAgIHN1bW1hcmlzZSh0b3RhbCA9IG4oKSkpICU+JQ0KICBtdXRhdGUoZnJlcSA9IG4vdG90YWwpDQoNCmZyZXF1ZW5jeQ0KYGBgDQoNCj4gIlRoaXMgaXMgYSBuaWNlIGFuZCB0aWR5IGRhdGEgZnJhbWUgYnV0IHdlIHdvdWxkIGFjdHVhbGx5IGxpa2UgdG8gcGxvdCB0aG9zZSBmcmVxdWVuY2llcyBvbiB0aGUgeC0gYW5kIHktYXhlcyBvZiBhIHBsb3QsIHNvIHdlIHdpbGwgbmVlZCB0byB1c2Ugc3ByZWFkKCkgZnJvbSB0aWR5ciBtYWtlIGEgZGlmZmVyZW50bHkgc2hhcGVkIGRhdGEgZnJhbWUuIC0tIGh0dHBzOi8vd3d3LnRpZHl0ZXh0bWluaW5nLmNvbS90d2l0dGVyLmh0bWwNCg0KcGl2b3Rfd2lkZXINCg0KYGBge3J9DQpmcmVxdWVuY3kgPC0gZnJlcXVlbmN5ICU+JSANCiAgc2VsZWN0KHNjcmVlbl9uYW1lLCB3b3JkLCBmcmVxKSAlPiUgDQogIHBpdm90X3dpZGVyKG5hbWVzX2Zyb20gPSBzY3JlZW5fbmFtZSwgdmFsdWVzX2Zyb20gPSBmcmVxKSAjLCB2YWx1ZXNfZmlsbCA9IDApDQoNCmZyZXF1ZW5jeSANCmBgYA0KDQojIyMgVmlzdWFsaXplDQoNCldvcmQgY2xvdWQNCg0KYGBge3Igd29yZC1jbG91ZH0NCnR3ZWV0c190b2tlbml6ZWQgJT4lIA0KICAjIGdyb3VwX2J5KHNjcmVlbl9uYW1lKSAlPiUgDQogIGNvdW50KHdvcmQsIHNvcnQgPSBUUlVFLCBuYW1lID0gImZyZXEiKSAlPiUgDQogIGhlYWQoNTAwKSAlPiUNCiAgZmlsdGVyKCFzdHJfZGV0ZWN0KHdvcmQsICJeXFxAIikpICU+JSANCiAgYW50aV9qb2luKHN0b3B3b3Jkc2xhbmdzKSAgJT4lIA0KICB3b3JkY2xvdWQyKHNpemUgPSAyKQ0KDQpgYGANCg0KRnJlcXVlbmNpZXMgYXJlIG9mdGVuIGJldHRlciBleHByZXNzZWQgd2l0aCBiYXJzDQoNCmBgYHtyIHdvcmQtZnJlcSBiYXJwbG90LCBmaWcuaGVpZ2h0PTd9DQp0d2VldHNfdG9rZW5pemVkICU+JSANCiAgY291bnQod29yZCwgc29ydCA9IFRSVUUsIG5hbWUgPSAiZnJlcSIpICU+JSANCiAgZmlsdGVyKCFzdHJfZGV0ZWN0KHdvcmQsICJeXFxAIikpICU+JSANCiAgc2xpY2VfaGVhZChuID0gMzApICU+JSANCiAgZ2dwbG90KGFlcyhmcmVxLCBmY3RfcmVvcmRlcih3b3JkLCBmcmVxKSkpICsNCiAgZ2VvbV9jb2woKQ0KDQp0d2VldHNfdG9rZW5pemVkICU+JSANCiAgY291bnQod29yZCwgc29ydCA9IFRSVUUsIG5hbWUgPSAiZnJlcSIpICU+JSANCiAgYW50aV9qb2luKHN0b3B3b3Jkc2xhbmdzKSAlPiUgICAgICAgICAgICAgICAgICAgICMgdHdpdHRlciBzdG9wIHdvcmRzLg0KICBmaWx0ZXIoIXN0cl9kZXRlY3Qod29yZCwgIl5cXEAiKSkgJT4lIA0KICBzbGljZV9oZWFkKG4gPSAzMCkgJT4lIA0KICBnZ3Bsb3QoYWVzKGZyZXEsIGZjdF9yZW9yZGVyKHdvcmQsIGZyZXEpKSkgKw0KICBnZW9tX2NvbCgpDQpgYGANCg0KT3IgcGxvdCB0aGUgd29yZCBmcmVxdWVuY3kgcmF0aW9zIG9mIHR3byB0d2VldGVycw0KaHR0cHM6Ly93d3cudGlkeXRleHRtaW5pbmcuY29tL3R3aXR0ZXIuaHRtbCN3b3JkLWZyZXF1ZW5jaWVzLTENCg0KYGBge3Igd29yZF9mcmVxIHBsb3R9DQpnZ3Bsb3QoZnJlcXVlbmN5LCBhZXMoUkdPTlpBTEVaMTk3OCwgQnJpY2tGYW5hdGljcykpICsNCiAgZ2VvbV9qaXR0ZXIoYWxwaGEgPSAwLjEsIHNpemUgPSAyLjUsIHdpZHRoID0gMC4yNSwgaGVpZ2h0ID0gMC4yNSkgKw0KICBnZW9tX3RleHQoYWVzKGxhYmVsID0gd29yZCksIGNoZWNrX292ZXJsYXAgPSBUUlVFLCB2anVzdCA9IDEuNSkgKw0KICBzY2FsZV94X2xvZzEwKGxhYmVscyA9IHNjYWxlczo6cGVyY2VudF9mb3JtYXQoKSkgKw0KICBzY2FsZV95X2xvZzEwKGxhYmVscyA9IHNjYWxlczo6cGVyY2VudF9mb3JtYXQoKSkgKw0KICBnZW9tX2FibGluZShjb2xvciA9ICJmaXJlYnJpY2siKQ0KDQpnZ3Bsb3QoZnJlcXVlbmN5LCBhZXMoQnJpY2tGYW5hdGljcywgZmFudGFzeXNpdGUpKSArDQogIGdlb21faml0dGVyKGFscGhhID0gMC4xLCBzaXplID0gMi41LCB3aWR0aCA9IDAuMjUsIGhlaWdodCA9IDAuMjUpICsNCiAgZ2VvbV90ZXh0KGFlcyhsYWJlbCA9IHdvcmQpLCBjaGVja19vdmVybGFwID0gVFJVRSwgdmp1c3QgPSAxLjUpICsNCiAgc2NhbGVfeF9sb2cxMChsYWJlbHMgPSBzY2FsZXM6OnBlcmNlbnRfZm9ybWF0KCkpICsNCiAgc2NhbGVfeV9sb2cxMChsYWJlbHMgPSBzY2FsZXM6OnBlcmNlbnRfZm9ybWF0KCkpICsNCiAgZ2VvbV9hYmxpbmUoY29sb3IgPSAiZmlyZWJyaWNrIikNCmBgYA0KDQoNCiMjIFdvcmQgVXNhZ2UNCg0KaHR0cHM6Ly93d3cudGlkeXRleHRtaW5pbmcuY29tL3R3aXR0ZXIuaHRtbCNjb21wYXJpbmctd29yZC11c2FnZQ0KDQoNCmBgYHtyfQ0KdHdlZXRzX2J5X3R3ZWV0ZXIgJT4lIA0KICBzdW1tYXJpc2UobWluX2RhdGUgPSBtaW4oY3JlYXRlZF9hdCksIG1heF9kYXRlID0gbWF4KGNyZWF0ZWRfYXQpKQ0KYGBgDQoNCg0KYGBge3J9DQp3b3JkX3JhdGlvcyA8LSB0d2VldHNfdG9rZW5pemVkICU+JQ0KICAjIGZpbHRlcihzY3JlZW5fbmFtZSA9PSAiQ0JCQ2VudDEiIHwgc2NyZWVuX25hbWUgPT0gIkFkYW1fQnJhZGZvcmQxNCIpICU+JQ0KICBmaWx0ZXIoc2NyZWVuX25hbWUgPT0gIkJyaWNrRmFuYXRpY3MiIHwgc2NyZWVuX25hbWUgPT0gImZhbnRhc3lzaXRlIikgJT4lIA0KICBmaWx0ZXIoIXN0cl9kZXRlY3Qod29yZCwgIl5AIikpICU+JQ0KICBjb3VudCh3b3JkLCBzY3JlZW5fbmFtZSkgJT4lDQogIGdyb3VwX2J5KHdvcmQpICU+JQ0KICBmaWx0ZXIoc3VtKG4pID49IDIpICU+JQ0KICB1bmdyb3VwKCkgJT4lDQogIHBpdm90X3dpZGVyKG5hbWVzX2Zyb20gPSBzY3JlZW5fbmFtZSwgdmFsdWVzX2Zyb20gPSBuLCB2YWx1ZXNfZmlsbCA9IDApICU+JQ0KICBtdXRhdGVfaWYoaXMubnVtZXJpYywgbGlzdCh+KC4gKyAxKSAvIChzdW0oLikgKyAxKSkpICU+JQ0KICBtdXRhdGUobG9ncmF0aW8gPSBsb2coQnJpY2tGYW5hdGljcyAvIGZhbnRhc3lzaXRlKSkgJT4lDQogIGFycmFuZ2UoZGVzYyhsb2dyYXRpbykpDQoNCndvcmRfcmF0aW9zDQpgYGANCg0KIyMjIGVxdWFsIHVzYWdlDQoNCmBgYHtyfQ0Kd29yZF9yYXRpb3MgJT4lIA0KICBhcnJhbmdlKGFicyhsb2dyYXRpbykpDQpgYGANCg0KDQpgYGB7ciB3b3JkLXVzYWdlfQ0Kd29yZF9yYXRpb3MgJT4lDQogIGdyb3VwX2J5KGxvZ3JhdGlvIDwgMCkgJT4lICAjIDwgMCkgJT4lDQogIHRvcF9uKDE1LCBhYnMobG9ncmF0aW8pKSAlPiUNCiAgdW5ncm91cCgpICU+JQ0KICBtdXRhdGUod29yZCA9IHJlb3JkZXIod29yZCwgbG9ncmF0aW8pKSAlPiUNCiAgZ2dwbG90KGFlcyh3b3JkLCBsb2dyYXRpbywgZmlsbCA9IGxvZ3JhdGlvIDwgMCkpICsNCiAgZ2VvbV9jb2woKSArICNzaG93LmxlZ2VuZCA9IEZBTFNFKSArDQogIGNvb3JkX2ZsaXAoKSArDQogIHlsYWIoImxvZyBvZGRzIHJhdGlvIChCcmlja0ZhbmF0aWNzL2ZhbnRhc3lzaXRlKSIpICsNCiAgIyBzY2FsZV9maWxsX2Rpc2NyZXRlKG5hbWUgPSAiIiwgbGFiZWxzID0gYygiQnJpY2tGYW5hdGljcyIsICJmYW50YXN5c2l0ZSIpKSArDQogIHNjYWxlX2ZpbGxfYnJld2VyKHBhbGV0dGUgPSAiRGFyazIiLCBuYW1lID0gIiIsIGxhYmVscyA9IGMoIkJyaWNrRmFuYXRpY3MiLCAiZmFudGFzeXNpdGUiKSkNCmBgYA0KDQojIyBGYXZvcml0ZXMgYW5kIHJldHdlZXRzDQoNCmh0dHBzOi8vd3d3LnRpZHl0ZXh0bWluaW5nLmNvbS90d2l0dGVyLmh0bWwjZmF2b3JpdGVzLWFuZC1yZXR3ZWV0cw0KDQojIyBDaGFuZ2VzIGluIHdvcmQgdXNlDQoNCmh0dHBzOi8vd3d3LnRpZHl0ZXh0bWluaW5nLmNvbS90d2l0dGVyLmh0bWwjY2hhbmdlcy1pbi13b3JkLXVzZQ0KDQojIyBUZXJtIERvY3VtZW50IE1hdHJpeA0KDQpodHRwczovL3d3dy50aWR5dGV4dG1pbmluZy5jb20vdGZpZGYuaHRtbCN0aGUtYmluZF90Zl9pZGYtZnVuY3Rpb24NCg0KYGBge3J9DQp0d2VldF93b3JkcyA8LSB0d2VldHNfYnlfdHdlZXRlciAlPiUgDQogIHNlbGVjdChzY3JlZW5fbmFtZSwgdGV4dCwgc3RhdHVzX2lkLCB1c2VyX2lkKSAlPiUgIA0KICB1bm5lc3RfdG9rZW5zKHdvcmQsIHRleHQsIHRva2VuID0gInR3ZWV0cyIpICU+JSANCiAgZmlsdGVyKCFzdHJfZGV0ZWN0KHdvcmQsICJeXFxAIikpICU+JQ0KICBmaWx0ZXIoIXN0cl9kZXRlY3Qod29yZCwgIl5odHRwIikpICU+JQ0KICBhbnRpX2pvaW4oc3RvcHdvcmRzbGFuZ3MpICAlPiUgDQogIGNvdW50KHdvcmQsIHR3ZWV0ZXIgPSBzY3JlZW5fbmFtZSwgc29ydCA9IFRSVUUpDQoNCnR3ZWV0X3dvcmRzDQoNCnRvdGFsX3dvcmRzIDwtIHR3ZWV0X3dvcmRzICU+JQ0KICBncm91cF9ieSh0d2VldGVyKSAlPiUNCiAgc3VtbWFyaXplKHRvdGFsID0gc3VtKG4pKSAlPiUgDQogIGFycmFuZ2UoLXRvdGFsKQ0KDQp0b3RhbF93b3Jkcw0KDQp0d2VldF93b3JkcyA8LSBsZWZ0X2pvaW4odHdlZXRfd29yZHMsIHRvdGFsX3dvcmRzKQ0KDQp0d2VldF93b3Jkcw0KYGBgDQoNCmBgYHtyfQ0KdHdlZXRfd29yZHMgJT4lIA0KICBiaW5kX3RmX2lkZih3b3JkLCB0d2VldGVyLCBuKQ0KYGBgDQoNCmBgYHtyIGZpZy5oZWlnaHQ9MTJ9DQp0d2VldF93b3JkcyAlPiUgDQogIGJpbmRfdGZfaWRmKHdvcmQsIHR3ZWV0ZXIsIG4pICU+JSANCiAgYXJyYW5nZShkZXNjKHRmX2lkZikpICU+JSANCiAgbXV0YXRlKHdvcmQgPSBmYWN0b3Iod29yZCwgbGV2ZWxzID0gcmV2KHVuaXF1ZSh3b3JkKSkpKSAlPiUgDQogIGZpbHRlcihuID4gMTApICU+JSANCiAgIyBncm91cF9ieSh0d2VldGVyKSAlPiUgDQogICMgdG9wX24oMikgJT4lIA0KICAjIHVuZ3JvdXAoKSAlPiUgDQogIGdncGxvdChhZXMod29yZCwgdGZfaWRmKSkgKw0KICBnZW9tX2NvbCgpICsNCiAgZmFjZXRfd3JhcCh+IHR3ZWV0ZXIpICsNCiAgY29vcmRfZmxpcCgpDQpgYGANCg0KDQojIyBSZXNvdXJjZSBsaXN0DQoNCi0gaHR0cDovL3d3dy5zdGhkYS5jb20vZW5nbGlzaC93aWtpL3RleHQtbWluaW5nLWFuZC13b3JkLWNsb3VkLWZ1bmRhbWVudGFscy1pbi1yLTUtc2ltcGxlLXN0ZXBzLXlvdS1zaG91bGQta25vdw0KDQotIGh0dHA6Ly9hbnRvbmlvLWZlcnJhcm8uZXUucG4vd29yZC1jbG91ZHMtaW4tci1wYWNrYWdlcy13b3JkY2xvdWQyLWFuZC10bS8NCg0KLSBodHRwczovL2pybm9sZC5naXRodWIuaW8vcXNzLXRpZHkvZGlzY292ZXJ5Lmh0bWwjdGV4dHVhbC1kYXRhDQoNCi0gaHR0cHM6Ly9yc3R1ZGlvLXB1YnMtc3RhdGljLnMzLmFtYXpvbmF3cy5jb20vMzE4NjdfODIzNjk4N2NmMGE4NDQ0ZTk2MmNjZDJhZWM0NmQ5YzMuaHRtbA0KDQotIG1pc2MNCg0KICAgIC0gaHR0cDovL3d3dy5jb29rYm9vay1yLmNvbS9NYW5pcHVsYXRpbmdfZGF0YS9Db252ZXJ0aW5nX2JldHdlZW5fZGF0YV9mcmFtZXNfYW5kX2NvbnRpbmdlbmN5X3RhYmxlcy8NCiAgICAtIGh0dHBzOi8vd3d3LnItYmxvZ2dlcnMuY29tL2hvdy10by1nZXQtdGhlLWZyZXF1ZW5jeS10YWJsZS1vZi1hLWNhdGVnb3JpY2FsLXZhcmlhYmxlLWFzLWEtZGF0YS1mcmFtZS1pbi1yLw0KICAgIC0gaHR0cHM6Ly93d3cucXVvcmEuY29tL0hvdy1kby1JLWdldC1hLWZyZXF1ZW5jeS1jb3VudC1iYXNlZC1vbi10d28tY29sdW1ucy12YXJpYWJsZXMtaW4tYW4tUi1kYXRhZnJhbWUNCiAgICAtIGh0dHBzOi8vd3d3LnF1b3JhLmNvbS9Ib3ctZG8teW91LWNyZWF0ZS1hLWNvcnB1cy1mcm9tLWEtZGF0YS1mcmFtZS1pbi1SDQogICAgDQoNCg==